#pragma once

#include <vector>
#include <string>
#include <optional>
#include <zenox/raiiutils.hpp>
#include <zenox/unicode.hpp>
#include <zenox/minilog.hpp>
#if defined(__unix__)
#include <spawn.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#elif defined(_WIN32)
#include <process.h>
#include <windows.h>
#endif

namespace zenox {

#if defined(__unix__)

typedef pid_t Pid;

inline Pid get_current_process_id() {
    return getpid();
}

inline std::optional<Pid> spawn_process(std::string const &path, std::vector<std::string> const &args, bool detached = false, bool use_path_env = true, bool add_argv0 = true) {
    pid_t pid;
    std::vector<const char *> argv;
    argv.reserve(args.size() + add_argv0);
    if (add_argv0) [[likely]] {
        argv.push_back(path.c_str());
    }
    for (auto const &a: args) {
        argv.push_back(a.c_str());
    }
    int err;
    if (use_path_env) {
        err = posix_spawnp(&pid, path.c_str(), NULL, NULL, (char *const *)argv.data(), environ);
    } else {
        err = posix_spawn(&pid, path.c_str(), NULL, NULL, (char *const *)argv.data(), environ);
    }
    if (err) [[unlikely]] {
        return std::nullopt;
    }
    if (detached) {
        setpgid(pid, pid);
    }
    return pid;
}

inline std::optional<int> wait_process(Pid pid) {
    int status;
    int ok = waitpid(pid, &status, 0);
    if (ok == -1) [[unlikely]] {
        return std::nullopt;
    }
    return status;
}

inline bool kill_process(Pid pid) {
    int ok = kill(pid, SIGKILL);
    if (ok == -1) [[unlikely]] {
        return false;
    }
    return true;
}

#elif defined(_WIN32)

typedef intptr_t Pid;

inline Pid current_process() {
    return _getpid();
}

inline std::optional<Pid> spawn_process(std::string const &path, std::vector<std::string> const &args, bool detached = false, bool use_path_env = true, bool add_argv0 = true) {
    intptr_t pid;
    std::vector<const char *> argv;
    argv.reserve(args.size() + add_argv0);
    if (add_argv0) [[likely]] {
        argv.push_back(path.c_str());
    }
    for (auto const &a: args) {
        argv.push_back(a.c_str());
    }
    int flag = _P_NOWAIT;
    if (detached) {
        flag = _P_DETACH;
    }
    if (use_path_env) {
        pid = _spawnvp(flag, path.c_str(), (char *const *)argv.data());
    } else {
        pid = _spawnv(flag, path.c_str(), (char *const *)argv.data());
    }
    if (pid == -1) [[unlikely]] {
        return std::nullopt;
    }
    return pid;
}

inline std::optional<int> wait_process(Pid pid) {
    int status;
    int ok = _cwait(&status, pid, _WAIT_CHILD);
    if (ok == -1) [[unlikely]] {
        return std::nullopt;
    }
    return status;
}

inline bool kill_process(Pid pid) {
    HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
    if (hProcess != NULL) [[likely]] {
        TerminateProcess(hProcess, 0);
        CloseHandle(hProcess);
        return true;
    }
    return false;
}

#else

typedef intptr_t Pid;

inline Pid current_process() {
    return 0;
}

inline std::optional<Pid> spawn_process(std::string const &path, std::vector<std::string> const &args, bool detached = false, bool use_path_env = true, bool add_argv0 = true) {
    return std::nullopt;
}

inline std::optional<int> wait_process(Pid pid) {
    return std::nullopt;
}

inline bool kill_process(Pid pid) {
    return false;
}

#endif

struct SubProcess {
    std::optional<Pid> m_pid;
    std::optional<int> m_status;
    value_initialize_on_move<bool> m_detached;

    SubProcess() = default;

    // warning: you need to do the zenox::u8path(cmd).string() yourself for each cmd and args!
    explicit SubProcess(std::string const &cmd, std::vector<std::string> const &args, bool detached = false) {
        m_pid = spawn_process(cmd, args, detached);
        m_detached = detached;
        m_status = std::nullopt;
    }

    void ok() const { // raise exception if status != 0
        ZENO_ASSERT_F(status(), _ == 0, "exit code not zero");
    }

    int status() const { // assumes exited()
        return *ZENO_ASSERT_F(m_status, _, "process not exited yet");
    }

    bool kill() { // assumes started()
        return kill_process(get_pid());
    }

    SubProcess &wait() {
        m_status = wait_process(get_pid());
        if (m_status.has_value()) [[likely]] {
            m_pid = std::nullopt;
        }
        return *this;
    }

    Pid get_pid() const { // assumes started()
        return *ZENO_ASSERT_F(m_pid, _, "process not started yet");
    }

    bool started() const {
        return m_pid.has_value();
    }

    bool exited() const {
        return m_status.has_value();
    }

    bool detached() const {
        return !m_pid.has_value() || m_detached;
    }
};

// usage: int status = subprocess("ls").arg("-l").argpath("/path/to/directory").run().wait().ok();
inline auto subprocess(std::string const &path) {
    struct privacy {};
    struct builder {
    private:
        std::string m_path;
        std::vector<std::string> m_args{};

    public:
        explicit builder(explicit_tag<privacy>, std::string path)
        : m_path(std::move(path))
        {}

        // only use this for regular strings
        builder &arg(std::string const &arg) {
            m_args.push_back(arg);
            return *this;
        }

        // must use this for file path
        builder &argpath(std::string const &path) {
            m_args.push_back(u8path(path).string());
            return *this;
        }

        // e.g.: argpath("--file=", filepath)
        builder &argpath(std::string const &prefix, std::string const &path) {
            m_args.push_back(prefix + u8path(path).string());
            return *this;
        }

        SubProcess run() const {
            return SubProcess(m_path, m_args, false);
        }

        SubProcess detach() const {
            return SubProcess(m_path, m_args, true);
        }
    };
    return builder{explicit_tag<privacy>{}, u8path(path).string()};
}

}
